// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-08-02

package commands

import (
	"context"
	_ "embed"
	"fmt"
	"gitee.com/go-libs/console"
	"gitee.com/go-libs/db-xorm/db"
	"gitee.com/go-libs/db-xorm/db/commands/base"
	"gitee.com/go-libs/db-xorm/db/commands/conf"
	"html/template"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

const DsnKey = "_gen:commands_"

var RegexMatchPkg = regexp.MustCompile(`([_a-zA-Z0-9]+)$`)

type Common struct {
	BaseDir                  string
	Dsn, DsnKey              string
	Export, Prefix           string
	ModelPath, ModelPkg      string
	Override                 bool
	TargetPath, TargetPkg    string
	ModuleName, ModuleFolder string
}

// BuildModule
// build module name and working relative folder.
func (o *Common) BuildModule(command *console.Command) (err error) {
	if o.ModuleName, o.ModuleFolder = command.GenerateModule(o.BaseDir); o.ModuleName == "" {
		err = fmt.Errorf(`can not parse module name from go.mod`)
		return
	}
	if o.ModuleFolder != "" {
		o.ModuleFolder = fmt.Sprintf(`/%s`, strings.TrimPrefix(o.ModuleFolder, "/"))
	}
	return
}

// BuildScript
// build command runner script.
func (o *Common) BuildScript(command *console.Command) string {
	return fmt.Sprintf(`go run main.go %s`, command.ToScript())
}

// ListTable
// returns a list for tables in a database.
func (o *Common) ListTable(ctx context.Context, command *console.Command, addColumnPkg bool) (tables []*base.Table, err error) {
	var (
		all     bool
		list    []*base.Table
		names   = make(map[string]string)
		named   = make(map[string]bool)
		session *db.Session
	)

	// Prepare
	// export names.
	if o.Export == "*" {
		all = true
	} else {
		for _, s := range strings.Split(o.Export, ",") {
			if s = strings.TrimSpace(s); s != "" {
				names[fmt.Sprintf(`%s%s`, o.Prefix, s)] = s
			}
		}
	}

	// Open
	// a connect session with exporter key.
	if session, err = db.Config.GetMaster(ctx, DsnKey); err != nil {
		return
	}

	// Prepare
	// table list.
	tables = make([]*base.Table, 0)

	// Select
	// all tables in database and columns of the table.
	if err = session.NoCascade().SQL("SHOW TABLE STATUS").Find(&list); err == nil {
		// Iterate
		// table in database.
		for _, table := range list {
			// Ignore
			// not allowed table.
			if !table.Name.Allow() {
				continue
			}

			// Ignore
			// not matched on names.
			if !all {
				if _, ok := names[table.Name.String()]; !ok {
					continue
				}
			}

			// Select
			// columns from a table.
			if err = session.NoCascade().SQL(fmt.Sprintf("SHOW FULL COLUMNS FROM `%s`", table.Name)).Find(&table.Columns); err != nil {
				break
			}

			// Format
			// table fields.
			o.FormatTable(command, table)
			for _, column := range table.Columns {
				o.FormatColumn(command, table, column, addColumnPkg)

				// Bind primary on table.
				if column.IsPrimaryKey {
					if convertor := conf.Model.Get(fmt.Sprintf(`%s.%s`, table.Name, column.Field), column.Type.Origin()); convertor != nil {
						table.PrimaryName = column.Field.ToLargeCamel()
						table.PrimaryFieldName = column.Field.String()
						table.PrimaryType = convertor.Type
					} else {
						column.IsPrimaryKey = false
					}
				}
			}

			// Append
			// to list.
			named[table.Name.String()] = true
			tables = append(tables, table)
		}
	}

	// Exported
	// name in command argument not found.
	for k := range names {
		if _, ok := named[k]; !ok {
			err = fmt.Errorf(`table not found: %s`, k)
			return
		}
	}

	// Empty result.
	if len(tables) == 0 {
		err = fmt.Errorf(`table not found in database`)
	}
	return
}

// ReadBaseDir
// read absolute path of the application.
func (o *Common) ReadBaseDir(command *console.Command, key, value string) (err error) {
	// Read
	// from command options.
	if opt, has := command.GetOption(key); has {
		if s := opt.ToString(); s != "" {
			value = s
		}
	}

	// Return error
	// if base dir is blank.
	if value == "" {
		err = fmt.Errorf(`base dir can not be blank`)
		return
	}

	// Convert
	// to absolute path.
	o.BaseDir, err = filepath.Abs(value)
	return
}

// ReadDsn
// read database source name.
func (o *Common) ReadDsn(command *console.Command, key string) (err error) {
	// Read dsn
	// from command options.
	if opt, has := command.GetOption(key); has {
		if k := opt.ToString(); k != "" {
			if cfg, ok := db.Config.Get(k); ok && len(cfg.Dsn) > 0 {
				o.Dsn = cfg.Dsn[0]
				o.DsnKey = k
				return
			}
			o.Dsn = k
			return
		}
	}

	// Use default
	// configuration of config files.
	k := "db"
	if cfg, ok := db.Config.Get(k); ok && len(cfg.Dsn) > 0 {
		o.Dsn = cfg.Dsn[0]
		o.DsnKey = k
		return
	}

	// Error read.
	err = fmt.Errorf(`dsn can not be blank`)
	return
}

// ReadExport
// read export names.
func (o *Common) ReadExport(command *console.Command, key string) (err error) {
	if opt, has := command.GetOption(key); has {
		if s := opt.ToString(); s != "" {
			o.Export = s
		}
	}
	return
}

// ReadModelPath
// read path for model files location.
func (o *Common) ReadModelPath(command *console.Command, key, value string) (err error) {
	// Read
	// from command options.
	if opt, has := command.GetOption(key); has {
		if s := opt.ToString(); s != "" {
			value = s
		}
	}

	// Return error
	// if base dir is blank.
	if value == "" {
		err = fmt.Errorf(`model path can not be blank`)
		return
	}

	// Convert
	// to absolute path.
	o.ModelPath = value

	// Read package name for model.
	if m := RegexMatchPkg.FindStringSubmatch(value); len(m) > 0 {
		o.ModelPkg = m[1]
		return
	}

	// Error read.
	err = fmt.Errorf(`can not parse package name for target`)
	return
}

// ReadOverride
// read override state for existing files.
func (o *Common) ReadOverride(command *console.Command, key string) (err error) {
	if opt, has := command.GetOption(key); has {
		o.Override = opt.GetSpecified()
	}
	return
}

// ReadPrefix
// read prefix for table name.
func (o *Common) ReadPrefix(command *console.Command, key string) (err error) {
	if opt, has := command.GetOption(key); has {
		if s := opt.ToString(); s != "" {
			o.Prefix = s
		}
	}
	return
}

// ReadTargetPath
// read path for generated files save to.
func (o *Common) ReadTargetPath(command *console.Command, key, value string) (err error) {
	// Read
	// from command options.
	if opt, has := command.GetOption(key); has {
		if s := opt.ToString(); s != "" {
			value = s
		}
	}

	// Return error
	// if base dir is blank.
	if value == "" {
		err = fmt.Errorf(`target path can not be blank`)
		return
	}

	// Convert
	// to absolute path.
	o.TargetPath = value

	// Read package for generated files save to.
	if m := RegexMatchPkg.FindStringSubmatch(value); len(m) > 0 {
		o.TargetPkg = m[1]
		return
	}

	// Error read.
	err = fmt.Errorf(`can not parse package name for target`)
	return
}

// RegisterDatabase
// adds a database connect configuration.
func (o *Common) RegisterDatabase() error {
	return db.Config.Add(DsnKey, &db.Database{
		Dsn:    []string{o.Dsn},
		Mapper: "snake",
	})
}

func (o *Common) FormatColumn(command *console.Command, table *base.Table, column *base.Column, addColumnPkg bool) {
	// Bind relation of a table.
	column.Table = table

	// Build comment list of a table.
	if len(column.Comment.Strings()) > 0 {
		column.CommentList = make([]template.HTML, 0)
		for _, s := range column.Comment.Strings() {
			if s = strings.TrimSpace(s); s != "" {
				column.CommentList = append(column.CommentList, template.HTML(s))
			}
		}
	}

	// Parse info fields.
	column.Datetime = template.HTML(time.Now().Format(time.RFC3339))
	column.Script = template.HTML(o.BuildScript(command))

	// Parse name fields.
	column.ExportName = column.Field.StructName(o.Prefix)
	column.ExportJson = column.Field.ToSnake()
	column.ExportOrm = column.Extra.Convert(column.Field)

	// Type convert.
	if convert := conf.Model.Get(fmt.Sprintf(`%s.%s`, table.Name, column.Field), column.Type.Origin()); convert != nil {
		column.ExportPkg = convert.Pkg
		column.ExportType = convert.Type

		// Append package.
		if addColumnPkg && column.ExportPkg != "" {
			table.Add(column.ExportPkg)
		}
	}

	column.IsDate = column.Type.IsDate()
	column.IsDatetime = column.Type.IsDatetime()
	column.IsDatetimeOnUpdate = column.Extra.IsDatetimeOnUpdate()
	column.IsPrimaryKey = column.Key.IsPrimary()
}

func (o *Common) FormatTable(command *console.Command, table *base.Table) {
	// Build comment list of a table.
	if len(table.Comment.Strings()) > 0 {
		table.CommentList = make([]template.HTML, 0)
		for _, s := range table.Comment.Strings() {
			if s = strings.TrimSpace(s); s != "" {
				table.CommentList = append(table.CommentList, template.HTML(s))
			}
		}
	}

	// Parse info fields.
	table.Datetime = template.HTML(time.Now().Format(time.RFC3339))
	table.Script = template.HTML(o.BuildScript(command))
}

func (o *Common) FormatTableAsModel(table *base.Table) {
	table.ModelName = table.Name.StructName(o.Prefix)
	table.ModelPkg = o.TargetPkg
}

func (o *Common) FormatTableAsService(table *base.Table) {
	table.ConnectionKey = o.DsnKey
	table.ModelName = table.Name.StructName(o.Prefix)
	table.ServiceName = fmt.Sprintf(`%sService`, table.ModelName)

	table.ModelPkg = o.ModelPkg
	table.ServicePkg = o.TargetPkg

	// table.Add(`context`)
	table.Add(`gitee.com/go-libs/db-xorm/db`)
	table.Add(`gitee.com/go-libs/db-xorm/db/src`)
	table.Add(fmt.Sprintf(`%s%s/%s`, o.ModuleName, o.ModuleFolder, o.ModelPath))
}
